package com.angel.architecture.initializer.types;

import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import org.apache.commons.collections15.Predicate;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.filefilter.SuffixFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.apache.log4j.Logger;

import com.angel.architecture.exceptions.NonBusinessException;

/**
 *
 * @author William
 *
 */
public class TypeInferencer{

	private static final Logger LOGGER = Logger.getLogger(TypeInferencer.class);

	private final Set<String> resolvedClasses;

	public TypeInferencer()
	{
		this(TypeInferencer.class.getClassLoader(), null);
	}

	public TypeInferencer(Collection<String> _extraDirectories)
	{
		this(TypeInferencer.class.getClassLoader(), _extraDirectories);
	}

	@SuppressWarnings("unchecked")
	public TypeInferencer(ClassLoader _classLoader, Collection<String> _extraDirectories)
	{
		this.resolvedClasses = new HashSet(500);

		long _currentTime = System.currentTimeMillis();
		Enumeration<URL> _enumeration;
		try
		{
			_enumeration = _classLoader.getResources("");
		}
		catch (IOException _ioException) {
			throw new NonBusinessException("Error while getting resources on classLoader.getResources(): " + _ioException.getMessage(), _ioException);
		}

		while(_enumeration.hasMoreElements()) {
			try {
				URL _url = (URL)_enumeration.nextElement();
				String urlString = _url.toExternalForm().replaceAll(" ", "%20");
				_url = new URL(urlString);
				workRecursivelyOn(new File(_url.toURI()));
			} catch (MalformedURLException e) {
				throw new NonBusinessException(e);
			} catch (URISyntaxException e) {
				throw new NonBusinessException(e);
			}

		}

		if (_extraDirectories != null)
			for (String _extraDirectory : _extraDirectories)
				workRecursivelyOn(new File(_extraDirectory));


		LOGGER.info("TypeInferencer found [" + this.resolvedClasses.size() + "] classes in [" + (System.currentTimeMillis() - _currentTime) + "] msecs");
	}

	private void workRecursivelyOn(File _file)
	{
		LOGGER.info("inferencing for location: [" + _file.getAbsolutePath() + "]...");
		workRecursivelyOn(_file, _file);
	}

	private void workRecursivelyOn(File _file, File _directory)
	{
		Iterator<?> i$;
		if (_file.isDirectory()) {
			LOGGER.debug("working on directory: [" + _file.getAbsolutePath() + "]...");

			for (i$ = FileUtils.listFiles(_file, new SuffixFileFilter(new String[] { "jar", "class" }), TrueFileFilter.INSTANCE).iterator(); i$.hasNext(); ) { Object _o = i$.next();
			workRecursivelyOn((File)_o, _directory);
			}
		}
		else if (_file.isFile()) {
			if (_file.getName().endsWith(".jar")) {
				handleJarFile(_file);
			}
			else
				handleClassFile(_file.getAbsolutePath(), _directory);
		}
	}

	private void handleJarFile(File _file)
	{
		LOGGER.debug("adding classes for jar: [" + _file.getAbsolutePath() + "]...");
		try
		{
			handleJarFileImpl(_file);
		}
		catch (IOException _ioException) {
			throw new NonBusinessException("Error while inferring on jar: [" + _file.getAbsolutePath() + "]: " + _ioException.getMessage(), _ioException);
		}
	}

	private void handleJarFileImpl(File _file) throws IOException
	{
		JarFile _jarFile = null;
		try
		{
			_jarFile = new JarFile(_file);
			Enumeration<?> _entryEnumeration = _jarFile.entries();

			while (_entryEnumeration.hasMoreElements())
				handleClassFile(((JarEntry)_entryEnumeration.nextElement()).getName(), null);
		}
		finally
		{
			if (_jarFile != null)
				_jarFile.close();
		}
	}

	private void handleClassFile(String _fileName, File _rootDirectory)
	{
		if ((!(_fileName.endsWith(".class"))) || (_fileName.indexOf("$") != -1)) {
			return;
		}

		String _className = _fileName;

		if (_rootDirectory != null)
			_className = _className.substring(_rootDirectory.getAbsolutePath().length() + 1);

		_className = _className.substring(0, _className.length() - 6);
		_className = _className.replace('/', '.');
		_className = _className.replace('\\', '.');

		this.resolvedClasses.add(_className);
		LOGGER.debug("class: [" + _className + "] was added");
	}

	public List<Class<?>> getClassesFor(String _packageRegExp, Predicate<Class<?>> _predicate)
	{
		List<Class<?>> _result = new ArrayList<Class<?>>();

		for (String _resolvedClass : this.resolvedClasses) try {
			Class<?> _class;
			while (true) {
				while ((_packageRegExp != null) && (!(_resolvedClass.matches(_packageRegExp))));
				_class = Class.forName(_resolvedClass);

				if ((_predicate == null) || (_predicate.evaluate(_class)))
					break;
			}
			_result.add(_class);
		}
		catch (Throwable _exception) {
			throw new NonBusinessException("Error while checking class [" + _resolvedClass + "]: " + _exception.getMessage(), _exception);
		}

		return _result;
	}

	public List<Class<?>> getClassesFor(String packageName) {
		List<Class<?>> _result = new ArrayList<Class<?>>();
		for(String resolvedClass: this.resolvedClasses){
			if(resolvedClass.matches(packageName)){
				try {
					_result.add(Class.forName(resolvedClass));
				} catch (Throwable e) {
					throw new NonBusinessException("Error while checking class [" + resolvedClass + "]: " + e.getMessage(), e);
				}
			}
		}
		return _result;
	}

	public Class<?> getClassWithSimpleName(String simpleName) {
		for(String resolvedClass: this.resolvedClasses){
			if(resolvedClass.endsWith(simpleName)){
				try {
					return Class.forName(resolvedClass);
				} catch (Throwable _exception) {
					throw new NonBusinessException("Error while checking class [" + resolvedClass + "]: " + _exception.getMessage(), _exception);
				}
			}
		}
		return null;
	}

}
